import Link from "next/link"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { PRIORITY_LABELS } from "@websensor/core/client"; import { EventRow } from "@/components/event-row"; import { Badge, Bar, Chip, Empty, ExtLink, Flag, HealthPill, PageHeader, Panel, Sparkline, Stat, Table, Td, TierBadge } from "@/components/ui"; import { api, type SensorRow } from "@/lib/api"; import { fmtDuration, fmtInt, fmtMs, fmtPct, fmtScore, relTime, typeLabel, untilTime, utcDateTime } from "@/lib/format"; export const dynamic = "force-dynamic"; export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { const { id } = await params; const d = await api.source(id); if (!d) return { title: "Source not found" }; return { title: `${d.source.name} — source intelligence`, description: d.source.description ?? `Sensors, quality, activity and events for ${d.source.name} (${d.source.domain}).`, alternates: { canonical: `/source/${d.source.id}` } }; } function qualityTone(v: number): "signal" | "warn" | "hot" | undefined { return v >= 80 ? "signal" : v >= 60 ? undefined : v >= 40 ? "warn" : "hot"; } /** Source intelligence page (spec §27, §48). */ export default async function SourcePage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const d = await api.source(id); if (!d) notFound(); const events = await api.events({ source: d.source.id, limit: 40 }); const src = d.source; const a = d.activity ?? {}; const q = d.quality; const score = a.activity_score ?? 0; const entityId = d.entities.find((e) => e.type === "organization" || e.id.startsWith("org_"))?.id; const daily = [...(d.daily ?? [])].sort((x, y) => x.day.localeCompare(y.day)).slice(-30); const sensorsUp = d.sensors.filter((s) => s.enabled && s.health === "UP").length; const rawTotal = q?.raw_changes ?? d.sensors.reduce((n, s) => n + (s.raw_changes ?? 0), 0); const meaningfulTotal = q?.meaningful_changes ?? d.sensors.reduce((n, s) => n + (s.meaningful_changes ?? 0), 0); const maxType = Math.max(1, ...(d.by_type ?? []).map((t) => t.n)); const extra = src as typeof src & { llm_enabled?: boolean; discover?: Record; fallback?: Record; rate_limit_per_min?: number | null; terms_reviewed_at?: string | null }; return ( <> Tier {src.tier} · {src.domain} {src.country && ( <> · {src.country} )} {src.first_party === false ? : } {src.enabled === false && DISABLED} } title={src.name} description={src.description ?? (src.first_party === false ? `Media / aggregator source: its reports are third-party evidence and never count as first-party confirmation.` : `Official channels of ${src.name}: every sensor below is an endpoint the organization itself publishes.`)} actions={ <> {src.categories.map((c) => {c})} {entityId && Entity & timeline →} Live → Website ↗ } />
{sensorsUp} / {d.sensors.length}} hint={`${d.sensors.filter((s) => s.health !== "UP").length} not UP`} tone={sensorsUp < d.sensors.length ? "warn" : undefined} />
{daily[0]!.day} → {daily[daily.length - 1]!.day} : undefined}> {daily.length ? (
{( [ { label: "Checks", key: "checks", tone: "muted" }, { label: "Raw changes", key: "raw_changes", tone: "info" }, { label: "Events", key: "events", tone: "signal" }, ] as const ).map((s) => { const values = daily.map((r) => Number(r[s.key] ?? 0)); const sum = values.reduce((x, y) => x + y, 0); return (
{s.label} {fmtInt(sum)}
); })}
) : ( Daily counters accumulate from the first check onward — nothing recorded for the last 30 days yet. )}
P0 critical · P1 major · P2 specialized · P3 low urgency}> {d.sensors.length === 0 ? ( No sensors yet — discovery validates feeds, sitemaps and status pages before creating sensors. ) : ( {d.sensors.map((s) => )}
)}
live →}> {events.items.length ? events.items.map((e) => ) : No meaningful events yet for this source.} {d.discovery.length > 0 && ( {d.discovery.map((c) => ( ))}
{c.kind} {c.url} {c.evidence} {c.score?.itemCount ?? "—"} {c.score?.value !== undefined ? fmtScore((c.score.value ?? 0) * 100) : "—"} {c.status} {relTime(c.found_at)}
)}
); } function SensorTr({ s }: { s: SensorRow }) { const runs = s.total_runs ?? 0; const nm = runs ? Math.round(((s.total_not_modified ?? 0) / runs) * 100) : null; const prio = s.priority ?? null; const cond = [s.has_etag || s.etag ? "etag" : null, s.has_last_modified || s.last_modified ? "lm" : null].filter(Boolean); const condTitle = [s.etag ? `ETag: ${s.etag}` : null, s.last_modified ? `Last-Modified: ${s.last_modified}` : null].filter(Boolean).join("\n") || "No conditional-request validators exposed"; return ( {s.name}
{s.url}
{s.type}
{s.connector}
{prio !== null ? P{prio} : —} {s.status ? : —} {s.current_interval_seconds ? fmtDuration(s.current_interval_seconds) : adaptive}
{s.base_interval_seconds ? `base ${fmtDuration(s.base_interval_seconds)}` : "no base"}
{s.last_check_at ? relTime(s.last_check_at) : "never"}
{untilTime(s.next_check_at)}
{s.last_status ? = 500 ? "text-danger" : s.last_status >= 400 ? "text-warn" : s.last_status === 304 ? "text-fg-subtle" : ""}>{s.last_status} : —} {s.last_error ?
{s.last_error}
: null} {cond.length ? cond.map((c) => {c}) : —} {fmtInt(s.checks_24h ?? 0)} chk
{s.changes_24h ?? 0} chg
{fmtInt(s.snapshot_count ?? 0)} {nm !== null ? `${nm}%` : —} {s.raw_changes ?? 0} / {s.meaningful_changes ?? 0} history → ); }